var gamejs = require("gamejs");
var BinaryHeap = require("gamejs/utils/binaryheap").BinaryHeap;
var matchModule = require("js/match");

/**
 * Enemy : an object representing an enemy.
 * It is created on a point and then it finds a path from this to a point of our planet.
 * If the enemy reaches its target, it removes some health to the player.
 * 
 * @extends Sprite
 */

/**
 * Creates an enemy in the given point.
 * 
 * @param {Match} match The Match object where this object will work.
 * @param {[number, number]} position The enemy's position, in format [x, y].
 * @throws {TypeError} If match isn't a Match object or position isn't in
 * the correct format.
 * @throws {RangeError} If the given position isn't inside the map
 * recovered from the given Match.
 * @throws {Error} If the enemy can't find a path in the map.
 *
 * @constructor
 */
var Enemy = exports.Enemy = function(match, position) {
	if( ! (match instanceof matchModule.Match)) {
		throw new TypeError("match isn't a valid Match object");
	}
	
	//if position isn't in the correct form the isInside method raises a TypeError
	if( ! match.getMap().isInside(position)) {
		throw new RangeError("Invalid coordinates");
	}
	
	Enemy.superConstructor.apply(this, arguments);
	this.currentMatch = match;
	
	//sets the path
	var targetPoint = [0, PLANET_CENTER + Math.round((Math.random() - 0.5) * PLANET_HEIGHT)];
	this.path = findPathWithStepLength(match.getMap(), position, targetPoint, 5);
	
	if(this.path === null) {
		throw new Error("Can't find a path.");
	}
	
	this.pathIndex = 0;
	this.speed = 50;//50 pixel per seconds
	this.maxHealth = 50;//starting health
	this.health = this.maxHealth;//actual health
	this.alpha = 50;//semitransparent
	this.damage = 1;//damage that this enemy inflict to the planet
	this.visibilityTime = 1000;//1 second of visibility
	this.detected = 0;
	this.prize = 50;//credits that the killing of this enemy give
	
	//images that the enemy may assume in his life circle
	this.images = {
		normal : imgPath + "squaredEnemy.png",
		detected : imgPath + "squaredDetectedEnemy.png",
		damaged: imgPath + "squaredDamagedEnemy.png"
	};
	this.image = gamejs.image.load(this.images.normal);
	this.image.setAlpha(1 - this.alpha / 100);
	
	//set the correct rect
	var dims = this.image.getSize();
	var topLeftCorner = [Math.round(position[X] - dims[WIDTH] / 2),
		Math.round(position[Y] - dims[HEIGHT] / 2)];
	this.rect = new gamejs.Rect(topLeftCorner, dims);
};

/**
 * Enemy extends Sprite.
 */
gamejs.utils.objects.extend(Enemy, gamejs.sprite.Sprite);

/**
 * Returns the Match object where this enemy work.
 *
 * @return {Match} The Match object where this enemy work.
 */
Enemy.prototype.getMatch = function() {
	return this.currentMatch;
}

/**
 * Returns the bounding rectangle of this enemy.
 * 
 * @return {gamejs.Rect} The bounding rectangle.
 */
Enemy.prototype.getBoundingRect = function() {
	return this.rect;
};

/**
 * Returns the coordinates of the center of this enemy.
 *
 * @returns {[number, number]} The coordinates of the center as vector [x, y].
 */
Enemy.prototype.getCenter = function() {
	return this.rect.center;
}

/**
 * Subtracts health points to this enemy, if the enemy dies gives the prize
 * to the player.
 * 
 * @param {number} hitRate The amount of health to subtract.
 * @return {number} The remaining amount of health.
 * @throws {RangeError} If hitRate is not greater than zero.
 */
Enemy.prototype.hit = function(hitRate) {
	if(hitRate < 0) {
		throw new RangeError("hitRate is not greater than zero");
	}
	this.health -= hitRate;
	if(this.health <= 0) {
		this.health = 0;
		this.kill();
		this.currentMatch.changeCredit(this.prize);
	}
	
	return this.health;
};

/**
 * Sets the detected flag, if detected sets the visibility timer.
 *
 * @param {boolean|undefined} detected True to turn detected mode on, false to turn it off;
 * if detected isn't defined takes true.
 */
Enemy.prototype.setDetected = function(detected) {
	if(detected || typeof detected == "undefined") {
		this.detected = this.visibilityTime;
		this.image = gamejs.image.load(this.images.detected);
	}
	else {
		this.detected = 0;
	}
};

/**
 * Returns the detected flag.
 *
 * @return {boolean} True if this enemy is detected, false otherwise.
 */
Enemy.prototype.isDetected = function() {
	return (this.detected > 0);
};

/**
 * Updates the position of this enemy, if needed updates the visibility timer.
 * If the enemy reaches its target, removes health points to the player.
 * 
 * @param {number} msDuration The time past from the last call, in ms.
 */
Enemy.prototype.update = function(msDuration) {
	//updates the position
	this.pathIndex += this.speed * (msDuration / 1000);
	var intIndex = Math.round(this.pathIndex);
	if(intIndex < this.path.length) {
		var centerPos = this.path[intIndex];
		var dim = this.image.getSize();
		var topleft = [centerPos[X] - dim[WIDTH] / 2, centerPos[Y] - dim[HEIGHT] / 2];
		this.rect = new gamejs.Rect(topleft, dim);
		
		var xPosition = this.path[intIndex][X];
		if(xPosition <= 2 * this.speed) {
			//this enemy is going to win, 2 seconds of fading
			this.image.setAlpha(1 - (xPosition / (2 * this.speed)));
		}
	}
	else {
		//this enemy win, change the player life then kill it
		var test = this.currentMatch.decreaseHealth(this.damage);
		console.log("health: "+test);
		this.kill();
	}
	
	
	//if detected, updates the timer and sets the correct image
	if(this.detected > 0) {
		this.detected -= msDuration;
		if(this.detected <= 0) {
			this.detected = 0;
			this.image = gamejs.image.load(this.images.normal);
			this.image.setAlpha(1 - this.alpha / 100);
		}
	}
};


/**
 * Returns an array of coordinates of navigable points from start to target, if any.
 * To improve the performances, this method first calculates the path with a distance of
 * stepLength between consecutive points, then it calculates each small path from a point to
 * its successive, giving an array of points distanced by 1.
 * 
 * @param {Map} map The Map object representing the map to cross.
 * @param {[number, number]} start Coordinates of the point from where to start, in [x, y] format.
 * @param {[number, number]} target Coordinates of the point to reach, in [x, y] format.
 * @param {number} stepLength The step length to use in the first computation of the path, to work
 * as expected it has to be reasonably smaller than the minimum number of pixel occupied by an obstacle
 * @return {Array} An array of coordinates of navigable points from start to target, if any;
 * null otherwise.
 * @throws {RangeError} If coordinates of start or target aren't inside the map.
 */
var findPathWithStepLength = function(map, start, target, stepLength) {
	var openList = new BinaryHeap(evaluateRemainingPath);
	var visited = new pointHashList();
	var closedList = new pointHashList();
	var currentStep = {};
	var theoreticallyLength = map.estimatedDistance(start, target);
	
	//this function evaluates the path from a point to the target,
	//it's used to keep the binary heap in the correct order
	function evaluateRemainingPath(step) {
		step.value = map.estimatedDistance(step.to, target) + step.length;
		return step.value;
	}
	
	//this function creates an object that provide a fast access to
	//a list of point, allowing to find a point in a small time
	function pointHashList() {
		var list = {};
		
		this.store = function(step) {
			list[step.to[X] + "-" + step.to[Y]] = step;
			return;
		}
		
		this.find = function(point) {
			return list[point[X] + "-" + point[Y]];
		}
		
		return this;
	}
	
	//this function returns true if the given point can reach the target with
	//a single step of length <= stepLength
	function reachableWithSmallerStep(point) {
		return (map.estimatedDistance(point, target) <= stepLength);
	}
	
	//this is an A* algorithm
	var find = false;
	currentStep = {to : start, from : null, length : 0};
	openList.push(currentStep);
	visited.store(currentStep);

	while((openList.size() > 0) && ( ! find)) {
		currentStep = openList.pop();
		//leaves a very big tolerance to avoid surrender with
		//a lot of obstacles
		if(currentStep.length > theoreticallyLength * 10) {
			//is doing too much steps
			break;
		}
		closedList.store(currentStep);
		
		if(reachableWithSmallerStep(currentStep.to)) {
			find = true;
			break;
		}
		var reachable = map.reachable(currentStep.to, stepLength);
		reachable.forEach(function (point) {
			if(closedList.find(point)) {
				return;
			}
			var known = visited.find(point);
			var newLength = map.estimatedDistance(currentStep.to, point) + currentStep.length;
			
			if(( ! known) || (known.length > newLength)) {
				if(known) {
					openList.remove(known);
				}
				var otherStep = {to : point, from : currentStep, length : newLength};
				openList.push(otherStep);
				visited.store(otherStep);
			}
		});
	}
	
	if(find) {
		//we have a path of points distanced by stepLength
		var path = [];
		//we don't have other big steps (of stepLength length) to do,
		//maybe we need some little steps (of 1 length)
		
		//start and target are inverted for convenience in the
		//adding of the points to the path in the right order
		var pathBetweenBigSteps = findPath(map, target, currentStep.to);
		if(pathBetweenBigSteps == null) {
			//algorithm can't find a path from the given points
			return null;
		}
		pathBetweenBigSteps.forEach(function (nextPoint) {
				path.unshift(nextPoint);
			});
		path.shift();//avoid duplicates
		
		while(currentStep.from != null) {
			//start and target are inverted for convenience in the
			//adding of the points to the path in the right order
			var pathBetweenBigSteps = findPath(map, currentStep.to, currentStep.from.to);
			if(pathBetweenBigSteps == null) {
				//algorithm can't find a path from the given points
				return null;
			}
			pathBetweenBigSteps.forEach(function (nextPoint) {
					path.unshift(nextPoint);
				});
			currentStep = currentStep.from;
		}
		
		return path;
	}
	else {
		//no path found
		return null;
	}
};

/**
 * Returns an array of coordinates of navigable points from start to target, if any.
 * 
 * @param {Map} map The map to cross.
 * @param {[number, number]} start Coordinates of the point from where to start.
 * @param {[number, number]} target Coordinates of the point to reach.
 * @return {Array} An array of coordinates of navigable points from start to target, if any;
 * null otherwise.
 * @throws {RangeError} If coordinates of start or target aren't inside the map.
 */
var findPath = function(map, start, target) {
	var openList = new BinaryHeap(evaluateRemainingPath);
	var visited = new pointHashList();
	var closedList = new pointHashList();
	var currentStep = {};
	var theoreticallyLength = map.estimatedDistance(start, target);
	
	//this function evaluates the path from a point to the target,
	//it's used to keep the binary heap in the correct order
	function evaluateRemainingPath(step) {
		step.value = map.estimatedDistance(step.to, target) + step.length;
		return step.value;
	}
	
	//this function creates an object that provide a fast access to
	//a list of point, allowing to find a point in a small time
	function pointHashList() {
		var list = {};
		
		this.store = function(step) {
			list[step.to[X] + "-" + step.to[Y]] = step;
			return;
		}
		
		this.find = function(point) {
			return list[point[X] + "-" + point[Y]];
		}
		
		return this;
	}
	
	//this function returns true if two points are equal
	function pointEqual(pointA, pointB) {
		return ((pointA[X] == pointB[X]) && (pointA[Y] == pointB[Y]));
	}
	
	//this is an A* algorithm
	var find = false;
	currentStep = {to : start, from : null, length : 0};
	openList.push(currentStep);
	visited.store(currentStep);

	while((openList.size() > 0) && ( ! find)) {
		currentStep = openList.pop();
		//leaves a small tolerance, theoretically there aren't
		//obstacles from start to target
		if(currentStep.length > theoreticallyLength * 4) {
			//is doing too much steps
			break;
		}
		closedList.store(currentStep);
		if(pointEqual(currentStep.to, target)) {
			find = true;
			break;
		}
		var adjacent = map.adjacent(currentStep.to);
		adjacent.forEach(function (point) {
			if(closedList.find(point)) {
				return;
			}
			var known = visited.find(point);
			var newLength = map.actualDistance(currentStep.to, point) + currentStep.length;
			
			if(( ! known) || (known.length > newLength)) {
				if(known) {
					openList.remove(known);
				}
				var otherStep = {to : point, from : currentStep, length : newLength};
				openList.push(otherStep);
				visited.store(otherStep);
			}
		});
	}
	
	if(find) {
		var path = [];
		//process currentStep to have an array of positions
		while(currentStep != null) {
			path.unshift(currentStep.to);
			currentStep = currentStep.from;
		}
		
		return path;
	}
	else {
		//no path found
		return null;
	}
};